Refactor webhook handling to accept all HTTP verbs

Andrew Cantino преди 10 години
родител
ревизия
794ae69b91

+ 1 - 1
Gemfile

@@ -48,12 +48,12 @@ platforms :ruby_18 do
48 48
 end
49 49
 
50 50
 group :development do
51
-  gem 'pry'
52 51
   gem 'binding_of_caller'
53 52
   gem 'better_errors'
54 53
 end
55 54
 
56 55
 group :development, :test do
56
+  gem 'pry'
57 57
   gem 'rspec-rails'
58 58
   gem 'rspec'
59 59
   gem 'shoulda-matchers'

+ 14 - 14
app/controllers/webhooks_controller.rb

@@ -1,39 +1,39 @@
1
-# This controller is designed to allow your Agents to receive cross-site Webhooks (posts).  When POSTed, your Agent will
2
-# have #receive_webhook called on itself with the POST params.
1
+# This controller is designed to allow your Agents to receive cross-site Webhooks (POSTs), or to output data streams.
2
+# When a POST or GET is received, your Agent will have #receive_webhook called on itself with the incoming params.
3 3
 #
4
-# Make POSTs to the following URL:
4
+# To implement webhooks, make POSTs to the following URL:
5 5
 #   http://yourserver.com/users/:user_id/webhooks/:agent_id/:secret
6 6
 # where :user_id is your User's id, :agent_id is an Agent's id, and :secret is a token that should be
7 7
 # user-specifiable in your Agent.  It is highly recommended that you verify this token whenever #receive_webhook
8 8
 # is called.  For example, one of your Agent's options could be :secret and you could compare this value
9 9
 # to params[:secret] whenever #receive_webhook is called on your Agent, rejecting invalid requests.
10 10
 #
11
-# Your Agent's #receive_webhook method should return an Array of [json_or_string_response, status_code].  For example:
11
+# Your Agent's #receive_webhook method should return an Array of [json_or_string_response, status_code, optional mime type].  For example:
12 12
 #   [{status: "success"}, 200]
13 13
 # or
14
-#   ["not found", 404]
14
+#   ["not found", 404, 'text/plain']
15 15
 
16 16
 class WebhooksController < ApplicationController
17 17
   skip_before_filter :authenticate_user!
18 18
 
19
-  def create
19
+  def handle_request
20 20
     user = User.find_by_id(params[:user_id])
21 21
     if user
22 22
       agent = user.agents.find_by_id(params[:agent_id])
23 23
       if agent
24
-        response, status = agent.trigger_webhook(params.except(:action, :controller, :agent_id, :user_id))
25
-        if response.is_a?(String)
26
-          render :text => response, :status => status || 200
27
-        elsif response.is_a?(Hash)
28
-          render :json => response, :status => status || 200
24
+        content, status, content_type = agent.trigger_webhook(params.except(:action, :controller, :agent_id, :user_id, :format), request.method_symbol.to_s, request.format.to_s)
25
+        if content.is_a?(String)
26
+          render :text => content, :status => status || 200, :content_type => content_type || 'text/plain'
27
+        elsif content.is_a?(Hash)
28
+          render :json => content, :status => status || 200
29 29
         else
30
-          head :ok
30
+          head(status || 200)
31 31
         end
32 32
       else
33
-        render :text => "agent not found", :status => :not_found
33
+        render :text => "agent not found", :status => 404
34 34
       end
35 35
     else
36
-      render :text => "user not found", :status => :not_found
36
+      render :text => "user not found", :status => 404
37 37
     end
38 38
   end
39 39
 end

+ 3 - 3
app/models/agent.rb

@@ -73,7 +73,7 @@ class Agent < ActiveRecord::Base
73 73
     # Implement me in your subclass of Agent.
74 74
   end
75 75
 
76
-  def receive_webhook(params)
76
+  def receive_webhook(params, method, format)
77 77
     # Implement me in your subclass of Agent.
78 78
     ["not implemented", 404]
79 79
   end
@@ -136,8 +136,8 @@ class Agent < ActiveRecord::Base
136 136
     message.gsub(/<([^>]+)>/) { Utils.value_at(payload, $1) || "??" }
137 137
   end
138 138
 
139
-  def trigger_webhook(params)
140
-    receive_webhook(params).tap do
139
+  def trigger_webhook(params, method, format)
140
+    receive_webhook(params, method, format).tap do
141 141
       self.last_webhook_at = Time.now
142 142
       save!
143 143
     end

+ 1 - 1
app/models/agents/twilio_agent.rb

@@ -78,7 +78,7 @@ module Agents
78 78
       "#{server_url}/users/#{self.user.id}/webhooks/#{self.id}/#{secret}"
79 79
     end
80 80
 
81
-    def receive_webhook(params)
81
+    def receive_webhook(params, method, format)
82 82
       if memory['pending_calls'].has_key? params['secret']
83 83
         response = Twilio::TwiML::Response.new {|r| r.Say memory['pending_calls'][params['secret']], :voice => 'woman'}
84 84
         memory['pending_calls'].delete params['secret']

+ 2 - 2
app/models/agents/webhook_agent.rb

@@ -36,9 +36,9 @@ module Agents
36 36
         "payload_path" => "payload"}
37 37
     end
38 38
 
39
-    def receive_webhook(params)
39
+    def receive_webhook(params, method, format)
40 40
       secret = params.delete('secret')
41
-      return ["Not Authorized", 401] unless secret == options['secret']
41
+      return ["Not Authorized", 401] unless secret == options['secret'] && method == "post"
42 42
 
43 43
       create_event(:payload => payload_for(params))
44 44
 

+ 1 - 1
config/routes.rb

@@ -31,7 +31,7 @@ Huginn::Application.routes.draw do
31 31
   match "/worker_status" => "worker_status#show"
32 32
 
33 33
   post "/users/:user_id/update_location/:secret" => "user_location_updates#create"
34
-  post "/users/:user_id/webhooks/:agent_id/:secret" => "webhooks#create"
34
+  match "/users/:user_id/webhooks/:agent_id/:secret" => "webhooks#handle_request"
35 35
 
36 36
 #  match "/delayed_job" => DelayedJobWeb, :anchor => false
37 37
   devise_for :users, :sign_out_via => [ :post, :delete ]

+ 51 - 8
spec/controllers/webhooks_controller_spec.rb

@@ -5,10 +5,12 @@ describe WebhooksController do
5 5
     cannot_receive_events!
6 6
     cannot_be_scheduled!
7 7
 
8
-    def receive_webhook(params)
8
+    def receive_webhook(params, method, format)
9 9
       if params.delete(:secret) == options[:secret]
10 10
         memory[:webhook_values] = params
11
-        ["success", 200]
11
+        memory[:webhook_format] = format
12
+        memory[:webhook_method] = method
13
+        ["success", 200, memory['content_type']]
12 14
       else
13 15
         ["failure", 404]
14 16
       end
@@ -24,31 +26,72 @@ describe WebhooksController do
24 26
 
25 27
   it "should not require login to trigger a webhook" do
26 28
     @agent.last_webhook_at.should be_nil
27
-    post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
29
+    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
28 30
     @agent.reload.last_webhook_at.should be_within(2).of(Time.now)
29 31
     response.body.should == "success"
30 32
     response.should be_success
31 33
   end
32 34
 
33 35
   it "should call receive_webhook" do
34
-    post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
35
-    @agent.reload.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
36
+    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
37
+    @agent.reload
38
+    @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
39
+    @agent.memory[:webhook_format].should == "text/html"
40
+    @agent.memory[:webhook_method].should == "post"
36 41
     response.body.should == "success"
42
+    response.headers['Content-Type'].should == 'text/plain; charset=utf-8'
37 43
     response.should be_success
38 44
 
39
-    post :create, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go"
45
+    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "not_my_secret", :no => "go"
40 46
     @agent.reload.memory[:webhook_values].should_not == { 'no' => "go" }
41 47
     response.body.should == "failure"
42 48
     response.should be_missing
43 49
   end
44 50
 
51
+  it "should accept gets" do
52
+    get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
53
+    @agent.reload
54
+    @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
55
+    @agent.memory[:webhook_format].should == "text/html"
56
+    @agent.memory[:webhook_method].should == "get"
57
+    response.body.should == "success"
58
+    response.should be_success
59
+  end
60
+
61
+  it "should pass through the received format" do
62
+    get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :json
63
+    @agent.reload
64
+    @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
65
+    @agent.memory[:webhook_format].should == "application/json"
66
+    @agent.memory[:webhook_method].should == "get"
67
+
68
+    post :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :xml
69
+    @agent.reload
70
+    @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
71
+    @agent.memory[:webhook_format].should == "application/xml"
72
+    @agent.memory[:webhook_method].should == "post"
73
+
74
+    put :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5", :format => :atom
75
+    @agent.reload
76
+    @agent.memory[:webhook_values].should == { 'key' => "value", 'another_key' => "5" }
77
+    @agent.memory[:webhook_format].should == "application/atom+xml"
78
+    @agent.memory[:webhook_method].should == "put"
79
+  end
80
+
81
+  it "can accept a content-type to return" do
82
+    @agent.memory['content_type'] = 'application/json'
83
+    @agent.save!
84
+    get :handle_request, :user_id => users(:bob).to_param, :agent_id => @agent.id, :secret => "my_secret", :key => "value", :another_key => "5"
85
+    response.headers['Content-Type'].should == 'application/json; charset=utf-8'
86
+  end
87
+
45 88
   it "should fail on incorrect users" do
46
-    post :create, :user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go"
89
+    post :handle_request, :user_id => users(:jane).to_param, :agent_id => @agent.id, :secret => "my_secret", :no => "go"
47 90
     response.should be_missing
48 91
   end
49 92
 
50 93
   it "should fail on incorrect agents" do
51
-    post :create, :user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go"
94
+    post :handle_request, :user_id => users(:bob).to_param, :agent_id => 454545, :secret => "my_secret", :no => "go"
52 95
     response.should be_missing
53 96
   end
54 97
 end

+ 10 - 2
spec/models/agents/webhook_agent_spec.rb

@@ -14,7 +14,7 @@ describe Agents::WebhookAgent do
14 14
     it 'should create event if secret matches' do
15 15
       out = nil
16 16
       lambda {
17
-        out = agent.receive_webhook('secret' => 'foobar', 'payload' => payload)
17
+        out = agent.receive_webhook({ 'secret' => 'foobar', 'payload' => payload }, "post", "text/html")
18 18
       }.should change { Event.count }.by(1)
19 19
       out.should eq(['Event Created', 201])
20 20
       Event.last.payload.should eq(payload)
@@ -23,7 +23,15 @@ describe Agents::WebhookAgent do
23 23
     it 'should not create event if secrets dont match' do
24 24
       out = nil
25 25
       lambda {
26
-        out = agent.receive_webhook('secret' => 'bazbat', 'payload' => payload)
26
+        out = agent.receive_webhook({ 'secret' => 'bazbat', 'payload' => payload }, "post", "text/html")
27
+      }.should change { Event.count }.by(0)
28
+      out.should eq(['Not Authorized', 401])
29
+    end
30
+
31
+    it "should only accept POSTs" do
32
+      out = nil
33
+      lambda {
34
+        out = agent.receive_webhook({ 'secret' => 'foobar', 'payload' => payload }, "get", "text/html")
27 35
       }.should change { Event.count }.by(0)
28 36
       out.should eq(['Not Authorized', 401])
29 37
     end

+ 19 - 0
spec/routing/webhooks_controller_spec.rb

@@ -0,0 +1,19 @@
1
+require 'spec_helper'
2
+
3
+describe "routing for webhooks" do
4
+  it "routes to handle_request" do
5
+    resulting_params = { :user_id => "6", :agent_id => "2", :secret => "foobar" }
6
+    get("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
7
+    post("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
8
+    put("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
9
+    delete("/users/6/webhooks/2/foobar").should route_to("webhooks#handle_request", resulting_params)
10
+  end
11
+
12
+  it "routes with format" do
13
+    get("/users/6/webhooks/2/foobar.json").should route_to("webhooks#handle_request",
14
+                                                           { :user_id => "6", :agent_id => "2", :secret => "foobar", :format => "json" })
15
+
16
+    get("/users/6/webhooks/2/foobar.atom").should route_to("webhooks#handle_request",
17
+                                                           { :user_id => "6", :agent_id => "2", :secret => "foobar", :format => "atom" })
18
+  end
19
+end